mirai - for Shiny and Plumber Applications

SatRdays London 2024

Charlie Gao

2024-04-27

mirai

未来

みらい       / mI ˈ ra ˈ i: /

  1. future

library(mirai)

m <- mirai({a + b}, a = 1, b = 2)

m
#> < mirai | $data >

m$data
#> 'unresolved' logi NA

call_mirai(m)

m$data
#> 3

But what’s so special about this? …

  1. Highly performant
  2. Simple and robust
  3. Designed for production

1: Highly Performant

  • Uses nanonext
nanonext::send
#> function(con, data, mode = c("serial", "raw", "next"), block = NULL)
#>  .Call(rnng_send, con, data, mode, block)

Built on NNG

Nanomsg Next Generation

  • State-of-the-art messaging and concurrency
  • C library (re-imagination of ZeroMQ)
  • Massively scalable
  • High throughput

Completely event-driven

Implementation completely devoid of polling loops:

while (unresolved(mirai)) {
  Sys.sleep(0.1)
}
  • Even for promises1

  • A world first!!

  • Special thanks to Joe Cheng (CTO Posit), creator of the Shiny framework

  • Instead uses: asynchronous callbacks from NNG

2: Simple and robust

Code simplicity

  • ~500 lines of code (in total)

  • Minimal interface (good defaults)

  • No ‘intimidating’ array of options

  • Easy to pick up

Code correctness

  • Explicit design: variables must be passed explicitly to the mirai
a <- 10
b <- 100
c <- 10000

m <- mirai(
  {
    y <- rnorm(a) * b + c
    rev(y)
  },
  a = a, b = b, c = c
)
  • no ‘automagical’ inferral (error-prone and makes code difficult to debug)

Code correctness

  • Convenience feature: allows passing an environment e.g. environment() (the calling environment)
a <- 10
b <- 100
c <- 1000

m <- mirai(
  {
    y <- rnorm(a) * b + c
    rev(y)
  },
  environment()
)

3: Designed for Production

Powers Crew and Targets

  • crew extends mirai to High-Performance Computing environments such as traditional clusters or the cloud
  • The default HPC backend for targets reproducible pipeline ecosystem by Will Landau

Powers Crew and Targets

  • Used on daily basis in the life sciences industry
  • Powers Bayesian simulations for clinical trials
  • Computations parallelised over thousands of HPC compute nodes

Integrated with Base R

Request by R Core (Luke Tierney) at R Project Sprint 2023

  • mirai added as the first alternative communications backend for the base parallel package.
library(parallel)
mirai::register_cluster()

cl <- makeCluster(2)
cl
#> < miraiCluster | ID: `0` nodes: 2 active: TRUE >

How did we get here?

ExtendedTask vs. Shiny Async

In 2017-2018, async programming introduced to R, and then Shiny, through the later and promises packages by Joe Cheng

  • Shiny Async “was never a truly satisfying solution”
  • Allows concurrent sessions (multiple users)
  • Async operations “infect” everything downstream
  • Did not solve intra-session concurrency and responsiveness
  • UNTIL NOW with ExtendedTask (elegant solution to free up the reactive cycle)

Sample Application

library(shiny)
library(bslib)
library(mirai)

ui <- page_fluid(
  numericInput("n", "Sample size (n)", 100),
  numericInput("delay", "Seconds to take for plot", 5),
  input_task_button("btn", "Plot uniform distribution"),
  plotOutput("plot")
)

server <- function(input, output, session) {
  extended_task <- ExtendedTask$new(
    function(...) mirai({Sys.sleep(y); runif(x)}, ...)
  ) |> bind_task_button("btn")
  observeEvent(input$btn, extended_task$invoke(x = input$n, y = input$delay))
  output$plot <- renderPlot(hist(extended_task$result()))
}

app <- shinyApp(ui = ui, server = server)
with(daemons(2), runApp(app))

https://shikokuchuo.net/mirai/articles/shiny.html

Steps to Use ExtendedTask

  1. [UI] create a bslib::input_task_button(). Nicer button automatically disabled during computation to prevent extra clicks
input_task_button("btn", "Plot uniform distribution")

Steps to Use ExtendedTask

  1. [server] create an ExtendedTask by calling ExtendedTask$new() on a function passing ... to a mirai() call, bind it to the button created above
extended_task <- ExtendedTask$new(
    function(...) mirai({Sys.sleep(y); runif(x)}, ...)
  ) |> bind_task_button("btn")
  1. [server] create an observer on the input button, which invokes the ExtendedTask with the named parameters for the mirai (passed via the ...)
observeEvent(input$btn, extended_task$invoke(x = input$n, y = input$delay))

Steps to Use ExtendedTask

  1. [server] create a render function for the output, which consumes the result of the ExtendedTask
output$plot <- renderPlot(hist(extended_task$result()))

Another Way

library(shiny)
library(bslib)
library(mirai)

ui <- page_fluid(
  numericInput("n", "Sample size (n)", 100),
  numericInput("delay", "Seconds to take for plot", 5),
  input_task_button("btn", "Plot uniform distribution"),
  plotOutput("plot")
)

server <- function(input, output, session) {
  extended_task <- ExtendedTask$new(
    function(x, y) mirai({Sys.sleep(y); runif(x)}, environment())
  ) |> bind_task_button("btn")
  observeEvent(input$btn, extended_task$invoke(input$n, input$delay))
  output$plot <- renderPlot(hist(extended_task$result()))
}

app <- shinyApp(ui = ui, server = server)
with(daemons(2), runApp(app))

Plumber

Using mirai with Plumber

library(plumber)
library(promises)
library(mirai)

pr() |>
  pr_get(
    "/echo",
    function(req, res) {
      mirai(
        { Sys.sleep(1L); list(status = 200L, body = list(msg = msg)) },
        msg = req[["HEADERS"]][["msg"]]
      ) %...>% (function(x) {
          res$status <- x$status
          res$body <- x$body
        })
    }
  ) |>
  pr_run(host = "127.0.0.1", port = 8985)

https://shikokuchuo.net/mirai/articles/plumber.html

Using mirai with Plumber

function(req, res) {
  mirai(
  {
    Sys.sleep(1L); list(status = 200L, body = list(msg = msg)) 
  },
  msg = req[["HEADERS"]][["msg"]]
  ) %...>% (function(x)
  {
    res$status <- x$status
    res$body <- x$body
  })
}

General solution:

  • Pass in only required parts of ‘req’ to the mirai
  • Assign the return value to ‘res’ in the promise action

Summary

  • mirai is the next generation parallel & distributed computing platform
  • First implementation of event-driven promises
  • First alternative communications backend for the parallel package

Thank you!